On this page

Skip to content

Coding Style

A few years ago, I searched for resources regarding C# Coding Style. However, as time passed, these resources often became difficult to find—either because the relevant blogs were taken down or because Microsoft had revised their versions, leaving some less common definitions uncertain. Therefore, I began recording my notes in this article for my own easy reference.

Microsoft provides a package for code style analysis; you can refer to the rules on GitHub at StyleCopAnalyzers. However, when I write code, I only follow these rules loosely, still adhering primarily to my early programming habits or rules I encountered previously. For example, while I might place left braces on a new line in other articles, I actually place them on the same line when writing code. This is because my early PHP formatting rules were based on Java, and Java places left braces on the same line.

WARNING

The coding style defined in this article is based on my personal preferences and does not necessarily represent the "best" way. It is best to follow your team's definitions or your own established habits.

Naming Rules

For naming, I follow C# casing conventions entirely. You can generally refer to the following two articles:

  • StyleCopAnalyzers: I have summarized them as follows:
    • Everything is Pascal Case, except for variables (Camel Case) and fields (depending on the situation).
    • Fields are Camel Case, except for the following cases:
      • Public and Internal fields use Pascal Case because they provide external access.
      • Constants and Static Readonly fields use Pascal Case because they represent constants.
      • Fields do not require prefixes like "", "m", or "s_".
    • Interfaces must start with "I".
    • For generic type parameters, if there is only one and it can be any type, use "T". If there are multiple, or if there are specific type requirements, use words starting with "T", such as List<T> and Dictionary<TKey, TValue>.
  • Casing Conventions for Acronyms:
    • There are two types of abbreviations:

      • Acronyms: Formed from the first letters of several words or phrases. For example, SHIELD is an acronym for Strategic Homeland Intervention, Enforcement and Logistics Division.
      • Abbreviations: Formed by taking a few letters from a word.
      • The casing of the first letter of an abbreviation depends on whether Pascal Case or Camel Case is used; subsequent letters are handled as follows:
      • For three or more letters, regardless of whether it is an acronym or abbreviation, the second letter onwards is lowercase, e.g., "Sql" or "sql".
      • For two-letter acronyms, they are all uppercase or all lowercase, e.g., "IO" or "io".
      • For two-letter abbreviations, the second letter onwards is lowercase, e.g., "Id" or "id".

      You can also refer to the article by Darkthread: "The Confusion of Acronym Casing: LINQHelper or LinqHelper?" (in Traditional Chinese).

WARNING

Although later versions of MSDN removed the casing rules for abbreviations, they still follow these rules during development. It is worth noting that these abbreviation rules only apply to C#. In reality, many languages define abbreviations differently. For example, in the JavaScript DOM, acronyms are always all uppercase or all lowercase, such as innerHTML.

Ordering Rules

StyleCopAnalyzers rules starting with SA12 are related to ordering. Except for methods, I generally follow these rules.

  • SA1201: Member ordering.
  • SA1202: Access modifier ordering:
    1. public
    2. internal
    3. protected internal
    4. protected
    5. private protected
    6. private
  • SA1203: Constant fields must appear before non-constant fields.
  • SA1204: Static members must appear above non-static members of the same type.
  • SA1206, SA1207: Declaration keyword ordering:
    1. Access Modifiers 1. protected 2. internal
    2. static
    3. Other Keywords
  • SA1208, SA1210: When using namespaces, those starting with System. should be placed first, and the rest should be sorted alphabetically.
  • SA1208, SA1210, SA1209, SA1211, SA1216, SA1217: Using ordering rules are as follows:
    1. Using Namespaces: Namespaces starting with System. are placed first, followed by others in alphabetical order.
    2. Using Static: Sorted by the full type name.
    3. Using Alias: Sorted by the alias name.
  • Aliases must be placed after using namespaces, and aliases are sorted alphabetically.
  • SA1212: Property and Indexer getters must be placed before setters.
  • SA1213: Event add accessors must be placed before remove accessors.
  • SA1214: Readonly fields must be placed before non-readonly fields.

Complete Example

csharp
using Namespace;
using static Namespace.StaticClassName;
using Namespace = Alias;

public class ClassName {
    // I only list these because I don't design Protected Fields or Public Non-static (Const) Fields
    #region Fields
    public const int ConstantName = 0;
    internal const int InternalConstantName = 0;
    private const int PrivateConstantName = 0;
    public readonly static int ReadonlyStaticFieldName = 0;
    private static int StaticFieldName = 0;
    private int fieldName = 0;
    #endregion

    #region Constructors
    static ClassName() { }

    public ClassName() { }

    protected ClassName() { }

    private ClassName() { }
    #endregion

    ~ClassName() { }

    // I only design Public Delegates
    public delegate int Delegate(int x);

    // I only design Public Events
    public event EventHandler Event;

    // I don't design private Properties
    #region Properties
    public int PropertyName { get; set; }

    internal int InternalPropertyName { get; set; }

    protected internal int ProtectedInternalPropertyName { get; set; }

    protected int ProtectedPropertyName { get; set; }
    #endregion

    // I don't design private Indexers
    #region Indexers
    public int this[byte i] { get; set; }
    internal int this[short i] { get; set; }
    protected internal int this[int i] { get; set; }
    protected int this[long i] { get; set; }
    #endregion

    // The following is the ordering under normal, unrelated conditions
    #region Methods
    public static void StaticMethodName() { }

    internal static void InternalStaticMethodName() { }

    public void MethodName() { }

    internal void InternalMethodName() { }

    protected internal void ProtectedInternalMethodName() { }

    protected void ProtectedMethodName() { }

    private void PrivateMethodName() { }
    #endregion

    public static bool operator ==(ClassName left, ClassName right) {
        return left == right;
    }
}

TIP

  • The code above uses region for readability in this sample. In practice, I do not use region to categorize code; I only use it to hide details I don't think developers need to see, allowing them to focus on the code that matters.

  • For method ordering, I do not follow the SA1202 (Access Modifiers) rule. Instead, I follow a rule I saw earlier: grouping homogeneous methods together. This way, when I see a method calling another, I can simply scroll down to see the implementation. Example:

    • Ordering for methods called only once.

      csharp
      public void Method() {
          MethodA();
          MethodB();
      }
      
      private void MethodA() {
          MethodA1();
      }
      
      private void MethodA1() { }
      
      private void MethodB() { }
    • Ordering for methods called repeatedly: Private methods are placed below the first calling method. If multiple similar methods call it, consider placing it below the last calling method.

      csharp
      public void MethodA() {
          SubMethod();
      }
      
      private void SubMethod() { }
      
      public void MethodB() {
          SubMethod();
      }
      
      public void MethodC() {
          SubMethod();
      }
      
      // OR
      public void MethodA1() {
          SubMethod1();
      }
      
      public void MethodB1() {
          SubMethod1();
      }
      
      public void MethodC1() {
          SubMethod1();
      }
      
      private void SubMethod1() { }
  • I do not order private methods according to SA1204 (Static and Non-static) because, as mentioned above, I group related methods together. Whether a private method is marked static largely depends on whether it uses instance members. It might have started as a non-static method because it used other private methods, and then became static after refactoring.

Comments

Single-line Comments

  • Format: Start with // followed by a space, e.g., // Your Comment.

TIP

Although this comment format is one I encountered when I first started programming, most modern guidelines do not strictly require it. However, within Microsoft, they still use this format, as seen in their source code or MSDN examples.

  • Position: Can appear above or to the right of the code.
  • Purpose: As a proponent of Clean Code, I prefer using comments to explain why something is done, rather than documenting what the code is doing.

Documentation Comments

When using Visual Studio, typing three / on a class, struct, interface, or member generates XML-structured documentation comments. These comments contain specific XML tags used to explain code and provide API documentation. For more details, refer to Documentation Comments.

If you need to tag generic types, you can use {} instead of <> because < and > might be misinterpreted in an XML structure. Example:

csharp
/// <seealso cref="Dictionary{TKey, TValue}"/>

Private members do not require documentation comments because they are not part of the API and are not exposed to other programs.

Task List

The compiler usually processes special comment keywords. Visual Studio provides "HACK", "TODO", "UNDONE", and "UnresolvedMergeConflict" by default. Comments containing these keywords appear in the Visual Studio Task List window to remind developers of pending tasks.

  • TODO: Used to mark features or items that need to be completed.
  • UNDONE: Used to mark features or tasks that are in progress but not yet finished.
  • HACK: Used to mark code blocks that need modification or correction. Usually a temporary solution added to solve a problem quickly. Once the problem is resolved, this code must be updated and the keyword removed.
  • UnresolvedMergeConflict: Related to version control conflicts; usually handled automatically by the version control system.

TIP

The meanings of "TODO" and "UNDONE" differ slightly: "TODO" usually indicates work to be done in the future, while "UNDONE" usually indicates work currently in progress but not yet completed.

Formatting

Left Braces

As mentioned, when I started with PHP, I preferred placing braces on the same line, so I find it unnatural to place them on a new line in C#. Keywords like else, catch, and finally are placed on the same line. Example:

csharp
if () {
} else if () {
} else {
}

try {
} catch {
} finally {
}

Spacing

The spacing I use in code is generally the same as Visual Studio's defaults. I insert spaces in the following situations:

  • After a ,, unless the , is at the end of a line.
  • After keywords in control flow statements.
  • Before and after the colon for base classes or interfaces in a class declaration.
  • After the ; in for statements.
  • Before and after operators, except for ++ and --, where no space is used between the operator and the variable.

Example:

csharp
for (int i = 0; i < 10; i++) {
}

Math.Max(1, 2);

Operator Line Breaks

When writing essays, it is generally recommended not to start a line with punctuation. Similarly, binary operators in mathematical expressions are easier to read when placed at the end of a line. This was my initial style, but I later saw arguments that placing binary operators at the beginning of a new line improves readability. According to ChatGPT, this style is called "out-of-line style," though I can no longer find the relevant articles. Notably, some programming languages have similar specifications, such as the Google Java Style Guide.

Overall, my formatting style aligns assignment operators (=) and binary operators with the text: assignment operators do not break at the start of a new line, while binary operators do. However, when a Lambda arrow is at a line break, I move it to the new line (unlike point 5 in the Google guide). This is partly because the code I've seen follows this, and partly to distinguish it from =, making it easier to tell if it's a field or a Lambda-style property.

Example:

csharp
class Test {
    // "=" stays at the end of the line, does not move to the new line
    private string field =
        "Field";

    // "=>" moves to the new line for better distinction
    public string Property
        => "Property";

    public void Method() {
        // "||" moves to the new line
        if (condition1
           || condition1
        ) {

        }

        // Binary operators move to the new line
        int a = b
            + c;
    }
}

TIP

Note that this is just a demonstration; in practice, the code length and complexity in the example do not require line breaks.

Ternary Operators

For short, non-nested ternary operators, keep them on a single line:

csharp
bool result = condition ? result1 : result2;

If they are too long, use line breaks in one of the following ways:

csharp
bool result = condition1
    ? result1 : result2;

bool result = condition1
    ? result1
    : result2;

For nested ternary operators, always use line breaks:

csharp
// Try to place sub-conditions in the ":" part to improve readability
bool result = condition1
    ? result1
    : condition2
        ? result2
        : condition3
            ? result3
            : result4;

// Another nested ternary operator style seen in ASP.NET Core source code
bool result = condition1 ? result1
    : condition2 ? result2
    : condition3 ? result3
    : result4;

The two nested ternary operator styles written as if-else:

csharp
if (condition1) {
    result = result1;
} else {
    if (condition2) {
        result = result2;
    } else {
        if (condition3) {
            result = result3;
        } else {
            result = result4;
        }
    }
}

if (condition1) {
    result = result1;
} else if (condition2) {
    result = result2;
} else if (condition3) {
    result = result3;
} else {
    result = result4;
}

TIP

As an aside, some languages like PHP provide an elseif keyword, but C#'s else if is simply a result of formatting.

Indentation

I am a "spaces" person. I use 4 spaces for code and 2 spaces for XML.

Empty Lines

  • No empty lines between fields; add one empty line between other members to improve readability and allow Visual Studio to display separator lines.
  • No empty lines at the beginning or end of blocks.
  • At most one empty line at a time.
  • Leave one empty line at the end of the file.

Example:

csharp
namespace namespace1 {
    // No empty line between namespace and class
    public class Class1 {
        // No empty line between class and field
        private string field;

        public string Property1 { get; set; }

        public string Property2 { get; set; }

        public void Method() {

        }
    }
}
// Add one empty line at the end of the code

TIP

In Unix/Linux environments, files end with a newline character (LF). If an editor opens a file without a trailing newline, it may treat the last line as incomplete, which can cause issues in some editors or version control systems. Therefore, Visual Studio's default formatting and many coding standards add an empty line at the end of the file.

Code Length

I set 80, 100, and 120 character separator lines in Visual Studio to help me evaluate whether code needs to be broken for readability.

TIP

Historically, code length was limited to 80 characters due to terminal and editor constraints. With modern screens, this is no longer a hard limit. However, I still avoid writing excessively long lines to maintain readability and avoid constant horizontal scrolling, especially when developing on smaller screens like laptops.

Change Log

  • 2022-11-06 Initial version created.